#include <errno.h>
#include <netdb.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/utsname.h>

#define MAXLINE 32
#define error_exit(msg) \
	do { perror(msg); exit(EXIT_FAILURE); } while (0)

void recv_all(int recvfd, socklen_t salen) {
	int n;
	char line[MAXLINE+1], buf[MAXLINE];
	socklen_t len;
	struct sockaddr *safrom;

	safrom = malloc(salen);
	for ( ; ; ) {
		len = salen;
		n = recvfrom(recvfd, line, MAXLINE, 0, safrom, &len);

		line[n] = 0;/* null terminate */
		printf("reply from %s:%d : %s", 
			inet_ntop(AF_INET, &((struct sockaddr_in *)safrom)->sin_addr, buf, sizeof(buf)),
			ntohs(((struct sockaddr_in *)safrom)->sin_port),
			line);	
	}
	free(safrom);
}

void send_all(int sendfd, struct sockaddr *sadest, socklen_t salen) {
	char line[MAXLINE];	
	struct utsname myname;

	if (uname(&myname) < 0) // uname获取当前内核名称和其它信息
		error_exit("uname error");
	snprintf(line, sizeof(line), "%s, %d\n", myname.release, getpid());

	for ( ; ; ) {
		sendto(sendfd, line, strlen(line), 0, sadest, salen);
		sleep(1);
	}
}

int udp_client(const char *host, const char *serv, struct sockaddr **saptr, socklen_t *lenp) {
	int sockfd, n;
	struct addrinfo	hints, *res, *ressave;

	/* 获取所查找主机的所有IP地址信息。
	 * getaddrinfo可处理以下转换：1.名字(test-pc)到地址(192.168.1.66)；2.服务(http)到端口(80)。 */
	bzero(&hints, sizeof(struct addrinfo));
	hints.ai_family = AF_UNSPEC; // AF_UNSPEC意味着函数返回的是适合任何协议族的地址
	hints.ai_socktype = SOCK_DGRAM; // SOCK_DGRAM意味着只返回数据报套接字信息
	if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) // 
		printf("udp_client error for %s, %s: %s", host, serv, gai_strerror(n));
	ressave = res;
	
	/* 尝试在上述查找到的所有IP地址上创建套接字 */
	do {
		sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (sockfd >= 0)
			break;	// success 
	} while ((res = res->ai_next) != NULL);
	if (res == NULL) { // 创建套接字失败
		printf("udp_client error for %s, %s", host, serv);
		return (-1); 
	}

	/* 保存创建的套接字地址信息并释放结果空间 */
	*saptr = malloc(res->ai_addrlen);
	memcpy(*saptr, res->ai_addr, res->ai_addrlen);
	*lenp = res->ai_addrlen;
	freeaddrinfo(ressave);

	return sockfd; 
}

/* 由协议族确定级别（level指定系统中解释选项的代码为“通用套接字代码”或为“IPv4协议代码”或为“IPv6协议代码”等） */
int family_to_level(int family) {
	switch (family) {
		case AF_INET:
			return IPPROTO_IP; // IPv4中MCAST_JOIN_GROUP套接字选项对应的level为IPPROTO_IP
		default:
			return -1;
	}
}

int mcast_join(int sockfd, const struct sockaddr *grp, socklen_t grplen, const char *ifname, u_int ifindex) {
#ifdef MCAST_JOIN_GROUP //内核是否支持MCAST_JOIN_GROUP套接字选项
	/* 处理索引 */
	struct group_req req;
	if (ifindex > 0) { // 调用者给定接口索引
		req.gr_interface = ifindex;
	} else if (ifname != NULL) { // 调用者给定接口名字，需使用if_nametoindex把名字转换成索引
		if ((req.gr_interface = if_nametoindex(ifname)) == 0) {
			errno = ENXIO;	/* i/f name not found */
			return(-1);
		}
	} else // 把接口索引置为0，告知内核去选择接口
		req.gr_interface = 0;
	
	/* 复制地址并调用setsockopt通过MCAST_JOIN_GROUP套接字选项执行加入多播组 */	
	if (grplen > sizeof(req.gr_group)) { // gr_group成员是一个sockaddr_storage结构
		errno = EINVAL;
		return -1;
	}
	memcpy(&req.gr_group, grp, grplen);
	return (setsockopt(sockfd, family_to_level(grp->sa_family), MCAST_JOIN_GROUP, &req, sizeof(req)));
#else
	/* 根据协议族处理加入多播组操作 */
	switch (grp->sa_family) {
		case AF_INET: { // IPv4协议族
			struct ip_mreq mreq;
			struct ifreq ifreq;

			/* 处理索引 */
			memcpy(&mreq.imr_multiaddr, // 把IPv4多播地址复制到一个ip_mreg结构中
				   &((const struct sockaddr_in *)grp)->sin_addr,
				   sizeof(struct in_addr));

			if (ifindex > 0) { // 调用者给定接口索引，那就用if_indextoname把接口名字存入一个ifreg结构中
				if (if_indextoname(ifindex, ifreq.ifr_name) == NULL) {
					errno = ENXIO;	/* i/f index not found */
					return(-1);
				}
				goto doioctl; // 接口名字存入ifreg结构后，发出ioctl请求获取单播地址
			} else if (ifname != NULL) { // 调用者给定接口名字
				strncpy(ifreq.ifr_name, ifname, IFNAMSIZ);
doioctl:
				if (ioctl(sockfd, SIOCGIFADDR, &ifreq) < 0) // ioctl的SIOCGIFADDR请求返回与该接口名字关联的单播地址
					return(-1);
				memcpy(&mreq.imr_interface, // 把IPv4单播地址复制到一个ip_mreg结构中
					   &((struct sockaddr_in *) &ifreq.ifr_addr)->sin_addr,
					   sizeof(struct in_addr));
			} else // 如果接口名字和接口索引都未给定，将接口设为通配地址，告知内核去选择接口
				mreq.imr_interface.s_addr = htonl(INADDR_ANY);

			/* 通过IP_ADD_MEMBERSHIP IPv4套接字选项执行加入多播组 */
			return(setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)));
		}
		default:
			errno = EAFNOSUPPORT;
			return(-1);
	}	
#endif
}

/* 获取指定套接字的协议族 */
int sockfd_to_family(int sockfd) {
	struct sockaddr_storage ss;
	socklen_t len;

	len = sizeof(ss);
	if (getsockname(sockfd, (struct sockaddr *)&ss, &len) < 0)
		return(-1);
	return(ss.ss_family);
}

/* 设置指定套接字的IP_MULTICAST_LOOP多播选项 */
int mcast_set_loop(int sockfd, int onoff) {
	u_char flag;	
	switch (sockfd_to_family(sockfd)) {
		case AF_INET: 
			flag = onoff;
			return(setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &flag, sizeof(flag)));
		default:
			errno = EAFNOSUPPORT;
			return (-1);
	}
}

int main(int argc, char **argv) {
	int sendfd, recvfd;
	const int on = 1;
	socklen_t salen;
	struct sockaddr *sasend, *sarecv;

	if (argc != 3)
		error_exit("usage: sendrecv <IP-multicast-address> <port#>");

	/* 创建两个数据报套接字，一个用于发送，一个用于接收。 */
	sendfd = udp_client(argv[1], argv[2], &sasend, &salen); // 支持主机名或服务名用作参数
	recvfd = socket(sasend->sa_family, SOCK_DGRAM, 0);
	
	/* 给接收套接字捆绑多播组和端口，例如239.255.1.2:8888 */
	setsockopt(recvfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); // 允许重用本地地址（即使存在连接也可重新绑定端口）
	sarecv = malloc(salen);
	memcpy(sarecv, sasend, salen);
	bind(recvfd, sarecv, salen);
	
	/* 接收套接字加入多播组 */
	mcast_join(recvfd, sasend, salen, NULL, 0);
	
	/* 禁止外出多播数据报的本地自环（回馈）。默认开启 */
	mcast_set_loop(sendfd, 0);

	/* 子进程不停接收数据报 */
	if (fork() == 0) 
		recv_all(recvfd, salen);

	/* 父进程不停发送数据报 */
	send_all(sendfd, sasend, salen);
	
	free(sarecv);
	exit(0);
}
